package org.jugile.web2;

import java.io.*;
import java.net.*;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.TimeZone;

import org.apache.log4j.Logger;
import org.jugile.util.Blob;
import org.jugile.util.Jugile;
import org.jugile.util.Timer;
import org.jugile.util.Props;


public class MiniServer extends Jugile implements IStore {
	static Logger log = Logger.getLogger(MiniServer.class);

	public Response serve(Request req) {
		//print("MiniServer.serve: " + req.getMethod() + " '" + req.getURI() + "' " );
		if (isStatic(req)) return serveFile( req.getURI(), req.getHeaders());
		Cycle e = new Cycle();
		e.hostname = "localhost";
		e.port = ""+myTcpPort;
		e.ipaddress = "127.0.0.1";
		return e.serve(req, this);
	}

	private boolean isStatic(Request req) {
		if (req.getURI().startsWith("/static/")) return true;
		if (req.getURI().endsWith(".ico")) return true;
		return false;
	}
	
	private Map<String,Object> store = null;
	private Map<String,Object> getStore() {
		if (store == null) store = new HashMap<String,Object>();
		return store;
	}
	public Object get(String key) { return getStore().get(key); }
	public void put(String key, Object value) { getStore().put(key, value); }
	

	public MiniServer(int port) throws IOException {
		myTcpPort = port;
		final ServerSocket ss = new ServerSocket( myTcpPort );
		Thread t = new Thread( new Runnable() {
			public void run() {
				try {
					while( true ) new HTTPSession( ss.accept());
				} catch ( IOException ioe )	{}
			}
		});
		t.setDaemon( true );
		t.start();
		// start worker
		Thread w = new Thread(new BaseWorker());
		w.setDaemon(true);
		w.start();
	}

	
	public static void main( String[] args ) throws Exception {
		// Change port if requested
		if (args.length > 0) {
			print("set props file: "+ args[0]);
			Props.filename(args[0]);
			print("port: " + Props.get("jugile.web.port"));
		}
		print("starting mini server...");

		Cycle.appInit();
				
		int port = parseIntSafe(Props.get("jugile.web.port")); //8888;
		if (port == 0) port = 8888;
		MiniServer nh = new MiniServer(port);
//		Timer t = new Timer();
//		print("loading domain");
//		Jugile.getDomainInstance();
//		print("loaded domain ms: " + t.stop());
		log.info( "Now serving files in port: " + port);
		log.info( "Hit Enter to stop.\n" );
		try { System.in.read(); } catch( Throwable thr ) {};
	}

	/**
	 * Handles one session, i.e. parses the HTTP request
	 * and returns the response.
	 */
	private class HTTPSession implements Runnable {
		public HTTPSession( Socket s ) {
			mySocket = s;
			Thread t = new Thread( this );
			t.setDaemon( true );
			t.start();
		}

		public void run() {
			try {
				InputStream is = mySocket.getInputStream();
				if ( is == null) return;
				//print("starting read");
				
				//Blob in = new Blob(is, 100);
				Request req = new Request();
				BufferedInputStream bis = new BufferedInputStream(is);
				
				// Read the request line
				String inLine = req.readLine(bis,0);
				if (inLine == null) return;
				StringTokenizer st = new StringTokenizer( inLine );
				if ( !st.hasMoreTokens()) sendError( Cycle.HTTP_BADREQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html" );
				String method = st.nextToken();
				req.setMethod(method);
				if ( !st.hasMoreTokens()) sendError( Cycle.HTTP_BADREQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html" );
				String uri = st.nextToken();
				
				// Decode parameters from the URI
				int qmi = uri.indexOf( '?' );
				if ( qmi >= 0 ) {
					req.decodeParms(uri.substring( qmi+1 ));
					req.setBody(null); // GET method - no body
					uri = req.decodePercent( uri.substring( 0, qmi ));
				} else uri = req.decodePercent(uri);
				req.setURI(uri);

				// If there's another token, it's protocol version,
				// followed by HTTP headers. Ignore version but parse headers.
				// NOTE: this now forces header names uppercase since they are
				// case insensitive and vary by client.
				if ( st.hasMoreTokens()) {
					String line = req.readLine(bis, 0);
					while ( line.trim().length() > 0 ) {
						int p = line.indexOf( ':' );
						req.setHeader( line.substring(0,p).trim().toLowerCase(), line.substring(p+1).trim());
						line = req.readLine(bis,0);
					}
				}

				// If the method is POST, there may be parameters
				// in data section, too, read it:
				if ( method.equalsIgnoreCase( "POST" ))	{
					//debugHeaders(req);
					if (req.isMultipart()) {
						req.parseMultipart(bis);
					} else {
						Integer bytes = Integer.parseInt(req.getHeader("content-length"));
						String postLine = req.readLine(bis, bytes);
						if (postLine != null) {
							postLine = postLine.trim();
							req.decodeParms(postLine);
						}
					}
				}

				// Ok, now do the serve()
				Response r = serve( req );
				if ( r == null ) sendError( Cycle.HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: Serve() returned a null response." );
				else sendResponse( r.status, r.mimeType, r.header, r.data );
				is.close();
			} catch ( IOException ioe )	{
				try {
					sendError( Cycle.HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
				} catch ( Throwable t ) {}
			} catch ( InterruptedException ie )	{
				// Thrown by sendError, ignore and exit the thread.
			}
		}

		private void debugHeaders(Request req) {
			log.debug("headers:");
			for (String key : req.getHeaderNames()) {
				log.debug("=== hdr " + key + "===" + req.getHeader(key));
			}
		}

		private void sendError( String status, String msg ) throws InterruptedException {
			sendResponse( status, Cycle.MIME_PLAINTEXT, null, new ByteArrayInputStream( msg.getBytes()));
			throw new InterruptedException();
		}

		/**
		 * Sends given response to the socket.
		 */
		private void sendResponse( String status, String mime, Properties header, InputStream data ) {
			try {
				if ( status == null )
					throw new Error( "sendResponse(): Status can't be null." );

				OutputStream out = mySocket.getOutputStream();
				PrintWriter pw = new PrintWriter( out );
				pw.print("HTTP/1.0 " + status + " \r\n");

				if ( mime != null )
					pw.print("Content-Type: " + mime + "\r\n");

				if ( header == null || header.getProperty( "Date" ) == null )
					pw.print( "Date: " + gmtFrmt.format( new Date()) + "\r\n");

				if ( header != null ) {
					Enumeration e = header.keys();
					while ( e.hasMoreElements()) {
						String key = (String)e.nextElement();
						String value = header.getProperty( key );
						pw.print( key + ": " + value + "\r\n");
					}
				}

				pw.print("\r\n");
				pw.flush();

				if ( data != null ) {
					byte[] buff = new byte[2048];
					while (true) {
						int read = data.read( buff, 0, 2048 );
						if (read <= 0) break;
						out.write( buff, 0, read );
					}
				}
				out.flush();
				out.close();
				if ( data != null ) data.close();
			} catch( IOException ioe ) {
				// Couldn't write? No can do.
				try { mySocket.close(); } catch( Throwable t ) {}
			}
		}
		private Socket mySocket;
	};

	private String encodeUri( String uri ) {
		String newUri = "";
		StringTokenizer st = new StringTokenizer( uri, "/ ", true );
		while ( st.hasMoreTokens()) {
			String tok = st.nextToken();
			if ( tok.equals( "/" ))
				newUri += "/";
			else if ( tok.equals( " " ))
				newUri += "%20";
			else {
				try { 
					newUri += URLEncoder.encode( tok, "UTF-8" );
				} catch ( UnsupportedEncodingException uee ) { fail(uee); };
			}
		}
		return newUri;
	}

	private int myTcpPort;

	public Response serveFile( String uri, Properties header) {
		uri = uri.replace("/static/", "");

		String dirname = Props.get("jugile.web.html");
		if (empty(dirname)) dirname = "./html";
		File homeDir = new File(dirname);
		
		// Make sure we won't die of an exception later
		if ( !homeDir.isDirectory())
			return new Response( Cycle.HTTP_INTERNALERROR, Cycle.MIME_PLAINTEXT,
			"INTERNAL ERRROR: serveFile(): given homeDir is not a directory." );

		// Remove URL arguments
		uri = uri.trim().replace( File.separatorChar, '/' );
		if ( uri.indexOf( '?' ) >= 0 ) uri = uri.substring(0, uri.indexOf( '?' ));

		// Prohibit getting out of current directory
		if ( uri.startsWith( ".." ) || uri.endsWith( ".." ) || uri.indexOf( "../" ) >= 0 )
			return new Response( Cycle.HTTP_FORBIDDEN, Cycle.MIME_PLAINTEXT,
			"FORBIDDEN: Won't serve ../ for security reasons." );

		File f = new File( homeDir, uri );
		if ( !f.exists())
			return new Response( Cycle.HTTP_NOTFOUND, Cycle.MIME_PLAINTEXT,
			"Error 404, file not found." );

		// List the directory, if necessary
		if ( f.isDirectory()) {
			// Browsers get confused without '/' after the
			// directory, send a redirect.
			if ( !uri.endsWith( "/" )) {
				uri += "/";
				Response r = new Response( Cycle.HTTP_REDIRECT, Cycle.MIME_HTML,
						"<html><body>Redirected: <a href=\"" + uri + "\">" +
						uri + "</a></body></html>");
				r.addHeader( "Location", uri );
				return r;
			}

			// First try index.html and index.htm
			if ( new File( f, "index.html" ).exists())
				f = new File( homeDir, uri + "/index.html" );
			else if ( new File( f, "index.htm" ).exists())
				f = new File( homeDir, uri + "/index.htm" );
			else {
				return new Response( Cycle.HTTP_FORBIDDEN, Cycle.MIME_PLAINTEXT,
				"FORBIDDEN: No directory listing." );
			}
		}

		try {
			// Get MIME type from file name extension, if possible
			String mime = null;
			int dot = f.getCanonicalPath().lastIndexOf( '.' );
			if ( dot >= 0 )
				mime = (String)theMimeTypes.get( f.getCanonicalPath().substring( dot + 1 ).toLowerCase());
			if ( mime == null )
				mime = Cycle.MIME_DEFAULT_BINARY;

			// Support (simple) skipping:
			long startFrom = 0;
			String range = header.getProperty( "Range" );
			if ( range != null ) {
				if ( range.startsWith( "bytes=" )) {
					range = range.substring( "bytes=".length());
					int minus = range.indexOf( '-' );
					if ( minus > 0 )
						range = range.substring( 0, minus );
					try	{
						startFrom = Long.parseLong( range );
					} catch ( NumberFormatException nfe ) {}
				}
			}

			FileInputStream fis = new FileInputStream( f );
			fis.skip( startFrom );
			Response r = new Response( Cycle.HTTP_OK, mime, fis );
			r.addHeader( "Content-length", "" + (f.length() - startFrom));
			r.addHeader( "Content-range", "" + startFrom + "-" + (f.length()-1) + "/" + f.length());
			return r;
		} catch( IOException ioe ) {
			return new Response( Cycle.HTTP_FORBIDDEN, Cycle.MIME_PLAINTEXT, "FORBIDDEN: Reading file failed." );
		}
	}

	private static Hashtable theMimeTypes = new Hashtable();
	static {
		StringTokenizer st = new StringTokenizer(
				"htm		text/html "+
				"html		text/html "+
				"js			application/x-javascript "+
				"css		text/css "+
				"txt		text/plain "+
				"asc		text/plain "+
				"gif		image/gif "+
				"jpg		image/jpeg "+
				"jpeg		image/jpeg "+
				"png		image/png "+
				"mp3		audio/mpeg "+
				"m3u		audio/mpeg-url " +
				"pdf		application/pdf "+
				"doc		application/msword "+
				"ogg		application/x-ogg "+
				"zip		application/octet-stream "+
				"exe		application/octet-stream "+
				"class		application/octet-stream " );
		while ( st.hasMoreTokens())
			theMimeTypes.put( st.nextToken(), st.nextToken());
	}

	private static java.text.SimpleDateFormat gmtFrmt;
	static {
		gmtFrmt = new java.text.SimpleDateFormat( "E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
		gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT"));
	}

}
